package com.dualsportmaps.android;

import java.io.BufferedInputStream;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Stack;
import java.util.Vector;

import net.osmand.map.ITileSource;
import net.osmand.osm.MapUtils;
import net.osmand.plus.BusyIndicator;
import net.osmand.plus.OsmandSettings;
import net.osmand.plus.ResourceManager;
import net.osmand.plus.ResourceManager.TileLoadDownloadRequest;
import net.osmand.plus.activities.MapActivity;
import net.osmand.plus.activities.OsmandApplication;
import net.osmand.plus.activities.SettingsActivity;

import org.apache.commons.logging.Log;

import android.app.ActivityManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.AssetManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Debug;
import android.os.PowerManager.WakeLock;
import android.util.FloatMath;
import android.widget.Toast;

import com.dualsportmaps.android.DsmMapDownload.DsmMapDownloadException;
import com.dualsportmaps.android.model.DsmMapTrack;
import com.dualsportmaps.android.views.DsmWeatherLayer.WeatherBitmap;

public class DsmResourceManager implements IDsmMapDownloadListener {
	public static final String APP_DIR = "dualsportmap/";
	public static final String MAP_DIR = "map/";
	
	private static final Log log = LogUtil.getLog(DsmResourceManager.class);
	private static DsmResourceManager manager = null;
	private OsmandApplication context;
	private MapActivity mapActivity;
	private DsmRemoteControlManager remote;
	private DsmMapDownloadServiceManager downloader;
	private DsmMapTileCachingServiceManager tileCacher;
	private Hashtable<String,DsmMapDownload> mapIndex = new Hashtable<String,DsmMapDownload>();
	public List<String> mapActive = new ArrayList<String>();
	public Hashtable<String,Object> mapActiveObjectCache = new Hashtable<String,Object>();
	public Hashtable<String,TileLoadDownloadRequest> tilesToCache = new Hashtable<String,TileLoadDownloadRequest>();
	private int progress = 0;
	public static boolean loadingUserLocations = false;
	public final AsyncLoadingThread asyncLoader = new AsyncLoadingThread();
	public static boolean realApp = true;
	private WakeLock cachingLock;
	public static final String CUSTOMRASTERMAPSUFFIX = " +";
	private int dalvikHeapLimit = 14;
	private int totalHeapLimit = 28;
	private int trackPointLimit = 85000;
	public static int perspectiveCacheLimit = 72;
	public static int normalCacheLimit = 56;
	private DsmResourceManager(OsmandApplication context) {
		this.context = context;
		if(realApp) {
			ActivityManager am = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
			if(android.os.Build.VERSION.SDK_INT > 10) {
				int heapSize = am.getLargeMemoryClass();
				dalvikHeapLimit = heapSize - 12;
				totalHeapLimit = heapSize - 4;
				if(heapSize >= 48) {
					trackPointLimit = 150000;
					perspectiveCacheLimit = 128;
					normalCacheLimit = 90;
				}
				if(heapSize >= 64) {
					trackPointLimit = 350000;
					perspectiveCacheLimit = 256;
					normalCacheLimit = 180;
				}
				if(heapSize >= 96) {
					perspectiveCacheLimit = 384;
					normalCacheLimit = 270;
				}
				if(heapSize >= 128) {
					perspectiveCacheLimit = 512;
					normalCacheLimit = 360;
				}
			} else {
				int heapSize = am.getMemoryClass();
				dalvikHeapLimit = heapSize - 18;
				totalHeapLimit = heapSize - 4;
				trackPointLimit = 85000;
			}
			
			remote = new DsmRemoteControlManager(context.getApplicationContext());
			cachingLock = CacheDsmMapTilesService.getLock(context);
			remote.doBindService();
			asyncLoader.start();
			DsmMapDownload.addListener(this);
			downloader = new DsmMapDownloadServiceManager(context.getApplicationContext());
			downloader.doBindService();
			if(DsmResourceManager.normalCacheLimit < 100) {
				tileCacher = new DsmMapTileCachingServiceManager(context.getApplicationContext());
				tileCacher.doBindService();
			}			
		}
	}
	public static OsmandApplication getApplication() {
		return manager.context;
	}
	public static Context getApplicationContext() {
		return manager.context.getApplicationContext();
	}
	public static MapActivity getMapActivity() {
		return manager.mapActivity;
	}
	public static DsmResourceManager getDsmResourceManager(OsmandApplication context) {
		if(manager == null)
			manager = new DsmResourceManager(context);
		return manager;
	}
	public static DsmResourceManager getDsmResourceManager() throws DsmResourceManagerException {
		if(manager == null)
			throw new DsmResourceManagerException();
		return manager;
	}
	public static DsmMapDownloadServiceManager getDsmMapDownloadServiceManager() {
		setupDownloadService();
		return manager.downloader;
	}
	public static void setupDownloadService() {
		if(!manager.downloader.mIsBound)
			manager.downloader.doBindService();
	}
	public static DsmMapTileCachingServiceManager getDsmMapTileCachingServiceManager() {
		if(DsmResourceManager.normalCacheLimit >= 100)
			return null;
		setupTileCacherService();
		return manager.tileCacher;
	}
	public static void setupTileCacherService() {
		if(!manager.tileCacher.mIsBound)
			manager.tileCacher.doBindService();
	}
	public static void setupRemote() {
		if(!manager.remote.mIsBound)
			manager.remote.doBindService();
	}
	public synchronized void setMapActivity(MapActivity mapActivity) {
		if(mapActivity != null) {
			DsmRemoteControlManager.addListener(mapActivity);
			setupRemote();
		} else {
			DsmRemoteControlManager.removeListener(this.mapActivity);
		}
		this.mapActivity = mapActivity;
	}
	public static int downloadcount = 0;
	public static int processcount = 0;
	public class AsyncLoadingThread extends Thread {
		Stack<Object> requests = new Stack<Object>();
		
		public AsyncLoadingThread(){
			super("Loader of DSM items"); //$NON-NLS-1$
		}
		public void requestToLoadWeatherImage(DsmWeatherImageDownloadRequest req){
			requests.push(req);
		}
		public void requestToLoadWeatherData(DsmWeatherDataDownloadRequest req){
			requests.push(req);
		}
		public void requestToLoadDsmMap(DsmMapDownloadRequest req){
			requests.push(req);
		}
		public void requestToProcessGPX(GPXProcessRequest req){
			requests.push(req);
		}
		@Override
		public void run() {
			while(true){
				try {
//					if(System.currentTimeMillis() > java.util.Date.parse("10/4/2011 23:59")) {
//						System.exit(0);
//					}
					int localprogress = 0;
					if(tilesToCache.size() == 0) {
						if(cachingLock != null)
							if(cachingLock.isHeld())
								cachingLock.release();
						Intent cacheIntent = new Intent(context, CacheDsmMapTilesService.class);
						context.stopService(cacheIntent);
					} else {
						if(cachingLock != null)
							if(!cachingLock.isHeld())
								cachingLock.acquire();
						localprogress = BusyIndicator.STATUS_BLACK;
					}
					synchronized(DsmResourceManager.this){
						if(!requests.isEmpty()){
							localprogress = BusyIndicator.STATUS_BLUE;
						}
					}
					if(DsmMapDownload.isSomethingBeingDownloaded()){
						localprogress = BusyIndicator.STATUS_GREEN;
					}
					while(!requests.isEmpty()){
						Object req = requests.pop();
						if (req instanceof DsmMapDownloadRequest) {
							DsmMapDownloadRequest r = (DsmMapDownloadRequest) req;
							DsmResourceManager.getDsmMapDownloadServiceManager().downloadMapFromWeb(r.dsmMapId);
						} else if (req instanceof DsmWeatherDataDownloadRequest) {
							DsmWeatherDataDownloadRequest r = (DsmWeatherDataDownloadRequest) req;
							r.wb.getWorldFile();
						}  else if (req instanceof DsmWeatherImageDownloadRequest) {
							DsmWeatherImageDownloadRequest r = (DsmWeatherImageDownloadRequest) req;
							if(r.wb.getImage()) {
								if(DsmResourceManager.getMapActivity() != null)
									DsmResourceManager.getMapActivity().getMapView().refreshMap();
							}
						} else if (req instanceof GPXProcessRequest) {
							processcount++;
							GPXProcessRequest r = (GPXProcessRequest) req;
							DsmResourceManager.getDsmMapDownloadServiceManager().processGPXFile(r.file.getAbsolutePath());
						}
					}
					if(downloadcount > 0 || loadingUserLocations) {
						localprogress = BusyIndicator.STATUS_GREEN;
					}
					if(processcount > 0) {
						localprogress = BusyIndicator.STATUS_BLACK;
					}
					progress = localprogress;
					sleep(750);
				} catch (InterruptedException e) {
					log.error(e, e);
				} catch (RuntimeException e){
					log.error(e, e);
				}
			}
		}
	};
	public static int getProgress() {
		return manager.progress;
	}
	
	public static class DsmMapDownloadRequest {
		String dsmMapId;
		public DsmMapDownloadRequest(String dsmMapId) {
			super();
			this.dsmMapId = dsmMapId;
		}
	}
	
	public static class GPXProcessRequest {
		File file;
		public GPXProcessRequest(File file) {
			super();
			this.file = file;
		}
	}
	
	public static class DsmWeatherImageDownloadRequest {
		WeatherBitmap wb;
		public DsmWeatherImageDownloadRequest(WeatherBitmap wb) {
			super();
			this.wb = wb;
		}
	}
	
	public static class DsmWeatherDataDownloadRequest {
		WeatherBitmap wb;
		public DsmWeatherDataDownloadRequest(WeatherBitmap wb) {
			super();
			this.wb = wb;
		}
	}
	
	@Override
	public void addDsmMap(final DsmMapDownload map) {
		if(map.getMap() != null) {
			if(map.getMap().getDsmMapId() != null) {
				if(mapIndex.containsKey(map.getMap().getDsmMapId())) {
					DsmMapDownload.removeDsmMapDownload(map.getMap().getDsmMapId());
				}
				mapIndex.put(map.getMap().getDsmMapId(),map);
				if(mapActivity != null) {
					mapActivity.runOnUiThread(new Runnable() {
						@Override
						public void run() {
							try {
								mapActivity.getMapView().refreshMap();
							} catch (Exception e) {
								// NULL POINTER ONE TIME
								e.printStackTrace();
							}
						}});
				}		
			}
		}
	}
	
	public boolean mapAlreadyLoaded(DsmMapDownload map) {
		if(map.getMap() != null)
			if(map.getMap().getDsmMapId() != null)
				if(mapIndex.containsKey(map.getMap().getDsmMapId())) {
					return true;
				}
		return false;
	}
	
	@Override
	public void removeDsmMap(String dsmMapId) {
		if(mapActive.contains(dsmMapId))
			hideMap(getDmd(dsmMapId));
		mapIndex.remove(dsmMapId);		
	}
	
	public DsmMapDownload getDmd(String dsmMapId) {
		try {
			if(mapIndex.containsKey(dsmMapId)) {
				DsmMapDownload retval = mapIndex.get(dsmMapId);
				if(retval==null) {
					retval = new DsmMapDownload(dsmMapId);
				}
				return retval;
			}
		} catch (DsmMapDownloadException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return null;
	}
	
	public List<DsmMapDownload> getDsmMaps() {
		List<DsmMapDownload> maps = new ArrayList<DsmMapDownload>();
		for(String dsmMapId : new Vector<String>(mapIndex.keySet())) {
			maps.add(getDmd(dsmMapId));
		}
		return maps;
	}
	
	public List<DsmMapDownload> getActiveDsmMaps() {
		List<DsmMapDownload> maps = new ArrayList<DsmMapDownload>();
		for(String dsmMapId : mapActive) {
			DsmMapDownload grr = getDmd(dsmMapId);
			if(grr != null)
				maps.add(grr);
		}
		return maps;
	}
	
	public void loadSavedMaps() {
		try {
			String activeMapsString = OsmandSettings.getActiveDsmMaps(getApplicationContext());
			List<String> blah = Arrays.asList(activeMapsString.split(",")); 
			blah.remove("");
			mapActive.addAll(blah);
		} catch (Exception e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}		
		new Thread(new Runnable(){
			@Override
			public void run() {
				for(String fileName : context.fileList()) {
					if(fileName.startsWith("dsmMap-")) {
						try {
							new DsmMapDownload(fileName.substring(7));
						} catch (DsmMapDownloadException e) {
							if(mapActivity != null)
								mapActivity.runOnUiThread(new Runnable() {
									@Override
									public void run() {
										Toast.makeText(mapActivity,"Map load failed",Toast.LENGTH_LONG).show();
									}
								});
						}
					}
				}
			}}).start();
	}
	
	public boolean dontprintMap() {
		long totalMapSize = 0L;
		for(DsmMapDownload map : getActiveDsmMaps()) {
			for(DsmMapTrack track : map.getTracks()) {
				totalMapSize += track.getDsmMapTrackGooglePoints().length();
			}
		}
		int mil = 1000000;
		long mem = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
		boolean overmemory = false;
		overmemory = (mem > dalvikHeapLimit*mil || mem + Debug.getNativeHeapAllocatedSize() > totalHeapLimit*mil || totalMapSize > trackPointLimit);
		if(overmemory ) {//TODO remove this line
			log.debug("Track points: " + totalMapSize + " | Memory: Allocated - " + mem + " | Native - " + Debug.getNativeHeapAllocatedSize());
		}
		return (overmemory && mapActive.size() > 1);
	}
	private boolean tooManyPoints() {
		long totalMapSize = 0L;
		for(DsmMapDownload map : getActiveDsmMaps()) {
			for(DsmMapTrack track : map.getTracks()) {
				totalMapSize += track.getDsmMapTrackGooglePoints().length();
			}
		}
		int mil = 1000000;
		return totalMapSize > mil/12;
	}
	public boolean showDsmMap(final DsmMapDownload map) {
		if(mapActivity != null) {
			if(dontprintMap()) {		
				mapActivity.runOnUiThread(new Runnable() {
					@Override
					public void run() {
						if(tooManyPoints())
							Toast.makeText(mapActivity,"You have " + trackPointLimit + " track points loaded. Please turn off another map before proceding.", Toast.LENGTH_SHORT).show();
						else 
							Toast.makeText(mapActivity,"Please turn off unneeded maps because you are running low on memory", Toast.LENGTH_SHORT).show();
					}
				});
				hideMap(map);
				return false;
			}
			else {
				mapActivity.runOnUiThread(new Runnable() {
					@Override
					public void run() {
						map.getEncodedPolylines(); //NEEDED TO SET map boundaries - should be done at server only problem for latest search results.
						if(!mapActive.contains(map.getMap().getDsmMapId())) {
							mapActive.add(map.getMap().getDsmMapId());
							saveMapActve();
							mapActivity.getMapView().refreshMap();
						}
					}
				});
				return true;
			}
		} else {
			if(!mapActive.contains(map.getMap().getDsmMapId())) {
				mapActive.add(map.getMap().getDsmMapId());
				saveMapActve();
			}
		}
		return true;
	}
	public void hideMap(final DsmMapDownload map) {
		if(map != null) {
			map.clearEncodedPolylines();
			mapActive.remove(map.getMap().getDsmMapId());
			saveMapActve();
		}
		if(mapActivity != null)
			mapActivity.runOnUiThread(new Runnable() {
				@Override
				public void run() {
					mapActivity.getMapView().refreshMap();
				}
			});
	}
	public void hideAllMaps() {
		for(DsmMapDownload map : DsmResourceManager.getDsmResourceManager((OsmandApplication)getApplication()).getActiveDsmMaps()) {
			if(map != null)
				map.clearEncodedPolylines();
		}
		mapActive.clear();
		saveMapActve();
		if(mapActivity != null)
			mapActivity.runOnUiThread(new Runnable() {
				@Override
				public void run() {
					mapActivity.getMapView().refreshMap();
				}
			});
	}
	public static Bitmap getIcon(String icon) throws Throwable {
		AssetManager am = getApplicationContext().getAssets();
		BufferedInputStream buf = new BufferedInputStream(am.open("maps/"+icon+".png"));
	    Bitmap bitmap = BitmapFactory.decodeStream(buf);
	    buf.close();
	    return bitmap;
	}
	public static Bitmap getScaledIcon(String icon, int size) throws Throwable {
	    return Bitmap.createScaledBitmap(DsmResourceManager.getIcon(icon),size,size,true);
	}
	public static class DsmResourceManagerException extends Exception {
		private static final long serialVersionUID = 1L;

		DsmResourceManagerException() {
			super("DsmResourceManager unavailable.");
		}
	}
	
	public void deleteMapCache(String dsmMapId) {
		for(String fileName : getApplication().fileList()) {
			if(fileName.startsWith("dsmMap") && fileName.endsWith(dsmMapId) && (dsmMapId.length() == 36 || dsmMapId.equals("SEARCHRESULTS") || dsmMapId.startsWith("GPX-"))) {
				DsmMapDownload.removeDsmMapDownload(dsmMapId);
				getApplication().deleteFile(fileName);
			}
		}
	}
	
	public void deleteMapCache() {
		for(String dsmMapId : new Vector<String>(mapIndex.keySet())) {
			DsmMapDownload.removeDsmMapDownload(dsmMapId);
		}		
		for(String fileName : getApplication().fileList()) {
			if(fileName.startsWith("dsmMap")) {
				getApplication().deleteFile(fileName);
			}
			hideAllMaps();
			mapActive.clear();
			mapIndex.clear();
			saveMapActve();
		}
	}
	
	private void saveMapActve() {
		try {
			StringBuilder sb = new StringBuilder();
			for (String x : mapActive)
				sb.append(x + ",");
			OsmandSettings.setActiveDsmMaps(getApplicationContext(),sb.toString());
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public void cacheTiles(String dsmMapId, boolean[] selectedMaps) {
		DsmMapDownload dsmmap = getDmd(dsmMapId);
		ResourceManager mgr = getApplication().getResourceManager();
		Map<String, String> entriesMap = SettingsActivity.getTileSourceEntries(context);
		final ArrayList<String> keys = new ArrayList<String>(entriesMap.keySet());
		final ArrayList<String> mapsToDownload = new ArrayList<String>();
		for(int x = 0; x<selectedMaps.length; x++) {
			if(selectedMaps[x])
				mapsToDownload.add(keys.get(x));
		}
		int count = 0;
		try {
			for(String mapname : mapsToDownload) {
				ITileSource map = OsmandSettings.getMapTileSource(mapname,OsmandSettings.getPrefs(context));
				for(EncodedPolyline ep : dsmmap.getEncodedPolylines()) {
					for(int zoom = map.getMinimumZoomSupported();zoom <=Math.min(map.getMaximumZoomSupported(), OsmandSettings.getMaximumLevelToDownloadTile(OsmandSettings.getPrefs(this.context)) ); zoom++) {
						List<SimpleTrackpoint> points = ep.getTrackPoints(zoom);
						for(SimpleTrackpoint point : points) {
							int x = (int) FloatMath.floor((float)MapUtils.getTileNumberX(zoom, point.getLongitude()));
							int y = (int) FloatMath.floor((float)MapUtils.getTileNumberY(zoom, point.getLatitude()));
							String ordImgTile = mgr.calculateTileId(map, x, y, zoom);
							String urlToGet = map.getUrlToLoad(x, y, zoom);
							if(!mgr.tileExistOnFileSystem(ordImgTile, map, x, y, zoom) && !tilesToCache.containsKey(urlToGet)) {
//								System.out.println("caching "+urlToGet);
								TileLoadDownloadRequest req = mgr.getTileLoadDownloadRequest(ordImgTile, map, x, y, zoom);
								tilesToCache.put(urlToGet,req);
								count++;
							}
						}
					}
				}
			}
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		if(count > 0) {
			Intent cacheIntent = new Intent(context, CacheDsmMapTilesService.class);
			cacheIntent.putExtra("mapname", dsmmap.getMap().getDsmMapName());
			cacheIntent.putExtra("count", count);
			context.startService(cacheIntent);
		}
	}
	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		DsmRemoteControlManager.removeListener(mapActivity);
		remote.doUnbindService();
		downloader.doUnbindService();
	}
	public void cancelTileCaching() {
		tilesToCache.clear();
	}
	
}
